ffmpeg example 视频文件封装和编码

您所在的位置:网站首页 ffmpeg 例子 ffmpeg example 视频文件封装和编码

ffmpeg example 视频文件封装和编码

2023-08-22 04:45| 来源: 网络整理| 查看: 265

今天学习 ffmpeg/doc/examples/muxing.c

程序编码10秒的音视频数据,写入参数指定的路径,文件后缀会决定文件的封装格式,使用的音视频编码器,调用的命令如下:

➜ examples git:(master) ✗ ./muxing_g /tmp/mux.mp4 ➜ examples git:(master) ✗ ./muxing_g /tmp/mux.mov 创建AVFormatContext //创建AVFormatContext, 根据文件后缀来推测output format avformat_alloc_output_context2(&oc, NULL, NULL, filename); if (!oc) { printf("Could not deduce output format from file extension: using MPEG.\n"); //无法推测output format, 使用mpeg avformat_alloc_output_context2(&oc, NULL, "mpeg", filename); } 给AVFormatContext添加 video / audio stream if (fmt->video_codec != AV_CODEC_ID_NONE) { add_stream(&video_st, oc, &video_codec, fmt->video_codec); have_video = 1; encode_video = 1; } if (fmt->audio_codec != AV_CODEC_ID_NONE) { add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec); have_audio = 1; encode_audio = 1; }

我们看看add_stream做了什么

/* Add an output stream. */ static void add_stream(OutputStream *ost, AVFormatContext *oc, const AVCodec **codec, enum AVCodecID codec_id) { AVCodecContext *c; int i; /* find the encoder */ //找到codec_id对应的编码器AVCodec *codec = avcodec_find_encoder(codec_id); //创建pkt ost->tmp_pkt = av_packet_alloc(); //创建AVStream ost->st = avformat_new_stream(oc, NULL); //设置stream的index ost->st->id = oc->nb_streams-1; //创建编码器上下文 c = avcodec_alloc_context3(*codec); //保存编码器上下文 ost->enc = c; switch ((*codec)->type) { case AVMEDIA_TYPE_AUDIO: //设置音频编码器上下文参数(码率,采样率,位深,声道布局等)               ... //设置stream的时间基 ost->st->time_base = (AVRational){ 1, c->sample_rate }; break; case AVMEDIA_TYPE_VIDEO: //设置编码器上下文的参数 c->codec_id = codec_id; //设置stream的时间基 ost->st->time_base = (AVRational){ 1, STREAM_FRAME_RATE }; c->time_base = ost->st->time_base;         //编码器码率,帧率,gop,分辨率, 设置         ... break; default: break; } /* Some formats want stream headers to be separate. */     //处理一下stream header if (oc->oformat->flags & AVFMT_GLOBALHEADER) c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; }

根据codec_id找到AVCodec

调用avformat_new_stream创建stream

设置stream的index

根据AVCodec创建编码器的上下文,配置编码器参数

设置stream的time_base

处理一下 stream headers 的标志位

音视频流创建好了,下一步为写入准备

上面创建好了stream, 创建了编码器上下文。open_video/open_audio继续为写入做准备

准备完成就打开文件准备写入

/* Now that all the parameters are set, we can open the audio and * video codecs and allocate the necessary encode buffers. */ if (have_video) open_video(oc, video_codec, &video_st, opt); if (have_audio) open_audio(oc, audio_codec, &audio_st, opt); //打印oc的信息 av_dump_format(oc, 0, filename, 1); /* open the output file, if needed */ if (!(fmt->flags & AVFMT_NOFILE)) { //打开文件 ret = avio_open(&oc->pb, filename, AVIO_FLAG_WRITE); if (ret < 0) { fprintf(stderr, "Could not open '%s': %s\n", filename, av_err2str(ret)); return 1; } }

看看open_video 做了什么

static void open_video(AVFormatContext *oc, const AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg) { int ret; AVCodecContext *c = ost->enc; AVDictionary *opt = NULL; //将opt_arg中的内容拷贝到opt中 av_dict_copy(&opt, opt_arg, 0); /* open the codec */ //打开编码器 ret = avcodec_open2(c, codec, &opt); //释放opt av_dict_free(&opt); if (ret < 0) { fprintf(stderr, "Could not open video codec: %s\n", av_err2str(ret)); exit(1); } /* allocate and init a re-usable frame */ //根据格式,宽高,创建一个复用的AVFrame ost->frame = alloc_picture(c->pix_fmt, c->width, c->height); if (!ost->frame) { fprintf(stderr, "Could not allocate video frame\n"); exit(1); } /* If the output format is not YUV420P, then a temporary YUV420P * picture is needed too. It is then converted to the required * output format. */ ost->tmp_frame = NULL; if (c->pix_fmt != AV_PIX_FMT_YUV420P) { //如果编码器对应的pix_fmt不是yuv420p, 创建一个yuv420p格式的AVFrame,保存在ost->tmp_frame中 ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, c->width, c->height); if (!ost->tmp_frame) { fprintf(stderr, "Could not allocate temporary picture\n"); exit(1); } } /* copy the stream parameters to the muxer */ //将编码器的参数拷贝到stream对应的编码参数中 ret = avcodec_parameters_from_context(ost->st->codecpar, c); if (ret < 0) { fprintf(stderr, "Could not copy the stream parameters\n"); exit(1); } }

打开编码器

申请资源,创建一个AVFrame用于存储编码前数据

调用avcodec_parameters_from_context将编码器的编码参数拷贝到stream的codecpar中

写音视频数据到文件

写文件头

/* Write the stream header, if any. */ //将流信息写文件头 ret = avformat_write_header(oc, &opt); if (ret < 0) { fprintf(stderr, "Error occurred when opening output file: %s\n", av_err2str(ret)); return 1; }

交替写音视频帧

while (encode_video || encode_audio) { /* select the stream to encode */ /* 交替写入编码后的音频和视频帧 视频写入结束或者video_st.next_pts time_base, audio_st.next_pts, audio_st.enc->time_base) flags & AVFMT_NOFILE)) /* Close the output file. */ avio_closep(&oc->pb); /* free the stream */ avformat_free_context(oc); write_video_frame static int write_video_frame(AVFormatContext *oc, OutputStream *ost) { return write_frame(oc, ost->enc, ost->st, get_video_frame(ost), ost->tmp_pkt); }

只是简单调用了write_frame

static int write_frame(AVFormatContext *fmt_ctx, AVCodecContext *c, AVStream *st, AVFrame *frame, AVPacket *pkt) { int ret; // send the frame to the encoder //将frame送给编码器去编码 ret = avcodec_send_frame(c, frame); if (ret < 0) { fprintf(stderr, "Error sending a frame to the encoder: %s\n", av_err2str(ret)); exit(1); } while (ret >= 0) { //从编码器中读出编码后的pkt ret = avcodec_receive_packet(c, pkt); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; else if (ret < 0) { fprintf(stderr, "Error encoding a frame: %s\n", av_err2str(ret)); exit(1); } /* rescale output packet timestamp values from codec to stream timebase */ //调整pkt的pts,pkt的时间基参编码器的时间基,将其转换为参考stream的时间基 av_packet_rescale_ts(pkt, c->time_base, st->time_base); //设置pkt的stream_index和stream对应的一致,音视频分别对应于不同的stream_index pkt->stream_index = st->index; /* Write the compressed frame to the media file. */ log_packet(fmt_ctx, pkt); //将pkt写入视频文件 ret = av_interleaved_write_frame(fmt_ctx, pkt); /* pkt is now blank (av_interleaved_write_frame() takes ownership of * its contents and resets pkt), so that no unreferencing is necessary. * This would be different if one used av_write_frame(). */ if (ret < 0) { fprintf(stderr, "Error while writing output packet: %s\n", av_err2str(ret)); exit(1); } } return ret == AVERROR_EOF ? 1 : 0; }

将AVFrame送给编码器去编码

读取pkt

调用av_packet_rescale_ts,调整pkt的时间戳,写入文件pkt的pts要以stream的time_base为基准

设置pkt的index

调用av_interleaved_write_frame将pkt写入文件

返回写入结果。当AVFrame为空时,会冲洗编码器,ret = AVERROR_EOF, 返回1, 结束写入

frame的每一帧数据,是来自于get_video_frame方法, 填充的假数据

static AVFrame *get_video_frame(OutputStream *ost) { AVCodecContext *c = ost->enc; /* check if we want to generate more frames */ if (av_compare_ts(ost->next_pts, c->time_base, STREAM_DURATION, (AVRational){ 1, 1 }) > 0) return NULL; /* when we pass a frame to the encoder, it may keep a reference to it * internally; make sure we do not overwrite it here */ if (av_frame_make_writable(ost->frame) < 0) exit(1); if (c->pix_fmt != AV_PIX_FMT_YUV420P) { /* as we only generate a YUV420P picture, we must convert it * to the codec pixel format if needed */ if (!ost->sws_ctx) { ost->sws_ctx = sws_getContext(c->width, c->height, AV_PIX_FMT_YUV420P, c->width, c->height, c->pix_fmt, SCALE_FLAGS, NULL, NULL, NULL); if (!ost->sws_ctx) { fprintf(stderr, "Could not initialize the conversion context\n"); exit(1); } } fill_yuv_image(ost->tmp_frame, ost->next_pts, c->width, c->height); sws_scale(ost->sws_ctx, (const uint8_t * const *) ost->tmp_frame->data, ost->tmp_frame->linesize, 0, c->height, ost->frame->data, ost->frame->linesize); } else { fill_yuv_image(ost->frame, ost->next_pts, c->width, c->height); } ost->frame->pts = ost->next_pts++; return ost->frame; }

调用av_compare_ts 判断是否继续生成新的frame,一开始规定了只写10秒钟的数据,超过了就不写了

av_frame_make_writable使当前的frame可写,被编码器引用的frame不可写,调用该方法如果被引用,内部会创建新buf,变成可写的

填充数据yuv,还做了缩放处理,暂不讨论

更新frame的pts

返回生成的frame

write_audio_frame

/* * encode one audio frame and send it to the muxer * return 1 when encoding is finished, 0 otherwise */ static int write_audio_frame(AVFormatContext *oc, OutputStream *ost) { AVCodecContext *c; AVFrame *frame; int ret; int dst_nb_samples; c = ost->enc; //获取音频帧 frame = get_audio_frame(ost); if (frame) { /* convert samples from native format to destination codec format, using the resampler */ /* compute destination number of samples */ /* 计算根据重采样后应该生成的采样个数 */ dst_nb_samples = av_rescale_rnd(swr_get_delay(ost->swr_ctx, c->sample_rate) + frame->nb_samples, c->sample_rate, c->sample_rate, AV_ROUND_UP); //由于没有修改采样率,只是修改了位深,采样个数保持不变 av_assert0(dst_nb_samples == frame->nb_samples); /* when we pass a frame to the encoder, it may keep a reference to it * internally; * make sure we do not overwrite it here */ //使ost->frame可写 ret = av_frame_make_writable(ost->frame); if (ret < 0) exit(1); /* convert to destination format */ //重采样 ret = swr_convert(ost->swr_ctx, ost->frame->data, dst_nb_samples, (const uint8_t **)frame->data, frame->nb_samples); if (ret < 0) { fprintf(stderr, "Error while converting\n"); exit(1); } frame = ost->frame; //重新计算音频pts frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base); //计算samples_count ost->samples_count += dst_nb_samples; } //将音频帧写入文件 return write_frame(oc, c, ost->st, frame, ost->tmp_pkt); }

准备AVFrame,调用get_audio_frame获取一个AVFrame,内部根据编码格式,填充pcm假数据

调用av_rescale_rnd计算重采样个数,如果数据源和送入编码器的音频的采样率不同,需要转换采样率,示例程序只是变了采样格式,没有更改采样率,采样个数不变

做重采样,重采样数据保存在ost->frame中

重采样后,需要更新frame的pts

调用write_frame编码,将数据写入文件

总结:

学习了通过应用libavformat将音视频数据编码封装到文件中

创建AVFormatContext

添加stream

配置编解码器,stream 参数

编码frame,生成pkt,更新pts

交替写音视频pkt

关闭编解码器,结束写文件



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3